<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>大文件上传</title>
  <style type="text/css">
    #progress {
      height: 20px;
      width: 300px;
      margin-bottom: 30px;
    }

    #progress span {
      display: block;
      height: 20px;
      width: 0;
      color: #fff;
      font-size: 12px;
    }

    .red {
      background-color: red;
    }

    .green {
      background-color: green;
    }
  </style>
</head>

<body>
  <h1>大文文件分片上传 之 断点续传</h1>
  <p>
    也是在分片上传的基础上，在上传的过程中如果网络中断或服务器中断的情况下，我们需要把已上传的片段信息 hash保存到本地，然后当网络恢复的时候，我们继续上传，那么继续上传的时候，
    我们得到保存的信息，通过 hash 值判断当前的分段是否上传过，跳过已上传的部分，只传未上传的部分。
    使用这种方式有个比较耗费资源的部分，就是需要对比分片的 hash 值，但是相对于我们重新上传消耗的时间来讲，这些可以忽略了。
    下面我们进行模拟断点续传，用每个 chunk 的索引（顺序值）代替 hash 值，构建一个简单对象，保存是否上传到了服务器。
  </p>
  <div>
    选择文件:
    <input type="file" id="f1" /><br /><br />
    <div id="progress">
      <span class="red"></span>
    </div>
    <button type="button" id="btn-submit">上传</button>
  </div>
  <script>
    const progressSpan = document.getElementById('progress').firstElementChild;
    /* *
     * 分割文件
     * file 文件
     * chunkSize 文件分割大小，默认1M
     * */
    function splitFile(file, chunkSize = 1) {
      const chunks = []; // 文件碎片集合
      let size = chunkSize * 1024 * 1024; // 分割的文件碎片大小
      if (file.size > chunkSize) { // 判断是否需要分割文件
        // 开始分割文件
        let start = 0,
          end = 0;
        while (true) {
          end += size;
          const blob = file.slice(start, end);
          start += size;
          if (!blob.size) { // 文件大小被分割完了，则停止分割
            break;
          }
          chunks.push(blob);
        }
      } else {
        chunks.push(file.slice(0));
      }
      return chunks;
    }
    /* *
     * 组装请求 FormData
     * file 文件
     * */
    async function submitUpload(file) {
      if (!file) {
        alert('请选择文件，再上传');
        return;
      }
      const fileStorageName = `${file.name}-${file.lastModified}`;
      const fileList = splitFile(file, 0.01);
      let chunkCount = fileList.length;
      let sendChunkCount = 0;
      const uploadedInfo = getUploadedFromStorage(fileStorageName);
      for (let index = 0; index < fileList.length; index++) {
        try {
          const isUpOk = uploadedInfo[index] ? `编号${index}：已上传` : `编号${index}：未上传`;
          console.log('isUpOk: ', isUpOk);
          if (uploadedInfo[index]) {
            sendChunkCount = index + 1; // 记录已上传的索引
            progress(sendChunkCount, chunkCount); // 更新进度条
            continue; // 如果已上传则跳过此次循环
          }
          const fd = new FormData();
          fd.append('f1', fileList[index]);
          fd.append('token', '1570615258206'); // 模拟token
          fd.append('index', index);
          await xhrSend(fd);
          sendChunkCount += 1;
          progress(sendChunkCount-1, chunkCount);
          //将成功信息保存到本地
          setUploadedToStorage(index, fileStorageName); // 更新进度条
          if (sendChunkCount === chunkCount) {
            console.log('上传完成，发送合并请求');
            const formD = new FormData();
            formD.append('type', 'merge');
            formD.append('chunkCount', chunkCount);
            formD.append('token', '1570615258206'); // 模拟token
            formD.append('filename', file.name);
            await xhrSend(formD);
            progress(sendChunkCount, chunkCount); // 更新进度条
          }
        } catch (error) {
          console.log('error: ', error);
        }
      }
    }
    /* *
    * 上传进度条
    * */
    function progress(sendChunkCount, chunkCount) {
      const completedPercent = (sendChunkCount / chunkCount * 100).toFixed(2);
      progressSpan.style.width = completedPercent + '%';
      progressSpan.innerHTML = completedPercent + '%';
      if (completedPercent > 90) {// 进度条变色
        progressSpan.classList.add('green');
      }
    }
    function clearProgressStyle() {
      progressSpan.style.width='0';
      progressSpan.classList.remove('green');
    }
    /* *
     * 接口调用
     * */
    function xhrSend(fd) {
      return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open('POST', 'http://localhost:8100/', true);
        xhr.onreadystatechange = (event) => {
          if (xhr.status === 200) { //注意 中断请求后readyState会立即变为4,所有存储都
            resolve();
          } else {
            reject('上传失败，请检查网络设置');
          }
        }
        xhr.onerror = (error) => {
          reject(error.message);
        }
        //注意 send 一定要写在最下面，否则 onprogress 只会执行最后一次 也就是100%的时候
        xhr.send(fd); //发送时  Content-Type默认就是: multipart/form-data; 
      })
    }
    //获得本地缓存的数据
    function getUploadedFromStorage(name) {
      return JSON.parse(localStorage.getItem(name) || '{}');
    }
    //写入缓存
    function setUploadedToStorage(index, name) {
      const obj = getUploadedFromStorage(name);
      obj[index] = true;
      localStorage.setItem(name, JSON.stringify(obj));
    }
    document.getElementById('btn-submit').addEventListener('click', (event) => {
      const file = document.getElementById('f1').files[0];
      submitUpload(file);
    })
    document.getElementById('f1').addEventListener('mousedown', (event) => {
      clearProgressStyle();
    })
  </script>
</body>

</html>